class NoiseArtComponent extends HTMLElement {
    constructor() {
        super();
        this._shadow = this.attachShadow({ mode: 'closed' });

        this._chartDiv = document.createElement('div');
        this._chartDiv.style.width = '100%';
        this._chartDiv.style.height = '100%';

        this._shadow.appendChild(this._chartDiv);
        this.chart = echarts.init(this._chartDiv);

        this.noise = this.getNoiseHelper();
        this.noise.seed(Math.random());

        this._config = {
            frequency: 500,
            offsetX: 0,
            offsetY: 100,
            minSize: 5,
            maxSize: 22,
            duration: 4000,
            color0: '#fff',
            color1: '#000',
            backgroundColor: '#fff'
        };
    }

    connectedCallback() {
        this.readAttributes();

        this.chart.setOption({
            backgroundColor: this._config.backgroundColor,
            graphic: {
                elements: this.createElements()
            }
        });

        // Resize support
        window.addEventListener('resize', this._resizeHandler = this.resizeChart.bind(this));

        if (ResizeObserver) {
            this._observer = new ResizeObserver(() => this.resizeChart());
            this._observer.observe(this.parentElement);
        }
    }

    disconnectedCallback() {
        window.removeEventListener('resize', this._resizeHandler);
        if (this._observer) this._observer.disconnect();
    }

    readAttributes() {
        for (const key in this._config) {
            const attr = this.getAttribute(key);
            if (attr !== null) {
                if (typeof this._config[key] === 'number') {
                    this._config[key] = Number(attr);
                } else {
                    this._config[key] = attr;
                }
            }
        }
    }

    createElements() {
        const elements = [];
        const width = this.chart.getWidth();
        const height = this.chart.getHeight();

        for (let x = 20; x < width; x += 40) {
            for (let y = 20; y < height; y += 40) {
                const rand = this.noise.perlin2(
                    x / this._config.frequency + this._config.offsetX,
                    y / this._config.frequency + this._config.offsetY
                );
                elements.push({
                    type: 'circle',
                    x, y,
                    style: { fill: this._config.color1 },
                    shape: { r: this._config.maxSize },
                    keyframeAnimation: {
                        duration: this._config.duration,
                        loop: true,
                        delay: (rand - 1) * this._config.duration,
                        keyframes: [
                            {
                                percent: 0.5,
                                easing: 'sinusoidalInOut',
                                style: { fill: this._config.color0 },
                                scaleX: this._config.minSize / this._config.maxSize,
                                scaleY: this._config.minSize / this._config.maxSize
                            },
                            {
                                percent: 1,
                                easing: 'sinusoidalInOut',
                                style: { fill: this._config.color1 },
                                scaleX: 1,
                                scaleY: 1
                            }
                        ]
                    }
                });
            }
        }

        return elements;
    }

    resizeChart() {
        if (this.chart) {
            this.chart.resize();
            this.chart.setOption({
                graphic: {
                    elements: this.createElements()
                }
            });
        }
    }

    getChartInstance() {
        return this.chart;
    }

    getNoiseHelper() {
        class Grad {
            constructor(x, y, z) {
                this.x = x; this.y = y; this.z = z;
            }
            dot2(x, y) { return this.x * x + this.y * y; }
        }

        const grad3 = [
            new Grad(1,1,0), new Grad(-1,1,0), new Grad(1,-1,0), new Grad(-1,-1,0),
            new Grad(1,0,1), new Grad(-1,0,1), new Grad(1,0,-1), new Grad(-1,0,-1),
            new Grad(0,1,1), new Grad(0,-1,1), new Grad(0,1,-1), new Grad(0,-1,-1)
        ];
        const p = [...Array(256).keys()];
        const perm = new Array(512);
        const gradP = new Array(512);

        function seed(seed) {
            if (seed > 0 && seed < 1) seed *= 65536;
            seed = Math.floor(seed);
            if (seed < 256) seed |= seed << 8;

            for (let i = 0; i < 256; i++) {
                let v = p[i];
                if (i & 1) v ^= seed & 255;
                else v ^= (seed >> 8) & 255;

                perm[i] = perm[i + 256] = v;
                gradP[i] = gradP[i + 256] = grad3[v % 12];
            }
        }

        function fade(t) {
            return t * t * t * (t * (t * 6 - 15) + 10);
        }

        function lerp(a, b, t) {
            return (1 - t) * a + t * b;
        }

        function perlin2(x, y) {
            let X = Math.floor(x), Y = Math.floor(y);
            x -= X; y -= Y;
            X &= 255; Y &= 255;

            let n00 = gradP[X + perm[Y]].dot2(x, y);
            let n01 = gradP[X + perm[Y + 1]].dot2(x, y - 1);
            let n10 = gradP[X + 1 + perm[Y]].dot2(x - 1, y);
            let n11 = gradP[X + 1 + perm[Y + 1]].dot2(x - 1, y - 1);

            let u = fade(x);
            return lerp(
                lerp(n00, n10, u),
                lerp(n01, n11, u),
                fade(y)
            );
        }

        seed(0); // default
        return { seed, perlin2 };
    }
}

customElements.define('noise-art', NoiseArtComponent);
